package com.autumn.workflow;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StreamUtils;

import com.autumn.platform.core.Utils;
import com.autumn.workflow.model.ProcessTrace;
import com.autumn.workflow.service.WorkflowService;

/**
 * 工作流演示流程
 * 
 * <pre>
 * 业务流程：这里使用简化了的信用卡申请的业务流程作为示例（注意：这里简化了很多业务，如归档操作等，实际流程需要和业务人员确认好）
 * 1. 申请：业务人员录入客户申请资料，提交申请，开始流程
 * 2. 受理：信用卡中心接受申请
 * 3. 定额：信用卡中心根据申请资料和用户信用情况，给用户制定信用卡额度，如果用户信用不够，直接拒绝，结束流程
 * 4. 审批：定好额度之后，由审批员审批，如果不通过，返回给定额人员；如果审批通过并且额度小于1万，流程流至“制卡”环节；如果审批通过但额度大于1万，再由另一名审批员审批复核
 * 5. 审批复核：对额度超过1万并通过初次审批的申请进行审批复核，通过流转至“制卡”环节；不通过则返回给定额人员
 * 6. 制卡：对通过审批的申请启动制卡过程
 * 7. 发卡：制卡成功后将卡片发给客户，流程结束
 * 
 * 可能会有的特殊情况（这里只是举例，实际流程需要和业务人员确认好）
 * 1. 信用特别好，额度超过1万，但是客户想快速拿到卡，这个时候初审通过后，可直接启动“制卡”流程
 * 2. 审批时，发现资料录入错误，直接退回到申请录入环节，而不是简单按流程退回至“定额”环节；重新录入后，不再经过受理、定额，而直接跳至审批环节
 * ...
 * 
 * 约定：由于流程图变化，需要重新发布流程图，会生成一系列新版本的流程定义相关数据，因此，尽量保证流程图的稳定性。
 * 1. 对于流程的分支条件，全部外化到流程图外，在流程图中只使用action一个变量，比如有三个分支，就分别使用三个条件${action==0}、${action==1}、${action==2}
 * 2. 并且特别使用${action==0}表示流程回退的情况。至于实际流程实例的action的值是多少，在流程引擎外运算好即可
 * 3. 流程任务的权限，Activit的模型比较简单，没有考虑机构的情况，因此弃用原生的控制体系，而采用ACT_TASK_AUTH_{USER|ROLE|ORG}表
 * 
 * </pre>
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class WorkflowDemo {

    /**
     * 示例流程的流程Key，也就是流程图配置中的id属性(<process id="workflow-demo" name="工作流演示流程" isExecutable="true">)
     */
    private static final String PROC_DEF_KEY = "workflow-demo";
    /**
     * 流程操作用户，这里使用同一个用户演示所有步骤，实际业务中通常需要不同用户操作
     */
    private static final String USER_ID = "43b97f4b-7dd4-4dbd-bd12-5be431b4f169";

    /**
     * 基于Activit封装的工作流服务，这个服务主要是封装了常用的工作流API，对于一些不太常用的，还是可以通过ProcessEngine获取相应的服务调用原生API
     */
    @Autowired
    private WorkflowService service;
    @Autowired
    private ProcessEngine engine;

    /**
     * <pre>
     * 发布流程定义，如果流程图有变更，通常需要开发人员参与变更
     * 1. 和业务人员确认好业务流程图
     * 2. 使用Activiti提供的插件绘制好流程图
     * 3. 调用deployProcessDefinition发布类路径下的流程图，一般只在开发时发布，也可以使用ZipInputStream发布压缩文件中的流程图
     * </pre>
     */
    @Ignore
    @Test
    public void testDeployProcessDefinition() {
        // 发布流程定义(类路径下的bpmn文件)，一次发布可能发布多个流程定义，但是这里演示只发布一个，而且推荐一次只发布一个
        Deployment deployment = service.deployProcessDefinition("config/processes/工作流演示流程.bpmn", USER_ID);

        // 下面的代码是用于验证发布的流程是否正确
        // 查询刚刚发布的流程定义
        List<ProcessDefinition> processDefinitions = engine.getRepositoryService().createProcessDefinitionQuery().deploymentId(deployment.getId()).list();
        // 断言这次发布只有一个流程定义
        Assert.assertTrue(null != processDefinitions && processDefinitions.size() == 1);
        // 断言流程定义Key
        Assert.assertEquals(PROC_DEF_KEY, processDefinitions.get(0).getKey());
    }

    /**
     * <pre>
     * 1. 开启一个流程实例
     * 2. 需要传入的参数：
     *   2.1 流程定义的Key
     *   2.2 启动的用户ID
     *   2.3 流程实例的业务键值
     *   2.4 流程启动时的相关变量，可以为空，一般传入需要和流程实例绑定的相关变量
     * </pre>
     */
    @Ignore
    @Test
    public void testStartProcessInstance() {
        Map<String, Object> variables = new HashMap<String, Object>();
        variables.put("var1", "value1");
        variables.put("var2", 2);
        String businessKey = "workflow-demo-key-" + Utils.randomNumeric(9);

        // 启动流程实例，在调用此API之前，应该已经写入业务数据表了
        ProcessInstance instance = service.startProcessInstanceByKey(PROC_DEF_KEY, variables, businessKey, USER_ID);

        // 根据流程图，启动一个流程实例，只会产生一个apply节点的任务
        this.assertAndReturnSingleTask(instance.getProcessInstanceId(), "apply");
        Map<String, Object> map = service.queryProcessInstanceVariables(instance.getProcessInstanceId());
        System.out.println(map);
        Assert.assertEquals(variables, map);
    }

    /**
     * <pre>
     * 流程测试1：开始==>申请==>受理
     * </pre>
     */
    //@Ignore
    @Test
    public void testProcess1() {
        Map<String, Object> variables = null;
        String businessKey = "workflow-demo-key-" + Utils.randomNumeric(9);

        // 启动流程实例，在调用此API之前，应该已经写入业务数据表了，根据流程图，启动一个流程实例，只会产生一个apply节点的任务
        ProcessInstance instance = service.startProcessInstanceByKey(PROC_DEF_KEY, variables, businessKey, USER_ID);
        String procInstId = instance.getProcessInstanceId();
        Task applyTask = this.assertAndReturnSingleTask(instance.getProcessInstanceId(), "apply");

        // 完成申请任务
        this.service.completeByTaskId(applyTask.getId(), USER_ID);
        // 根据流程图，完成apply之后，该流程实例将会产生一笔accept(受理)节点的任务
        this.assertAndReturnSingleTask(procInstId, "accept");
        System.out.println(this.service.queryCurrentNodeId(procInstId));
        System.out.println(this.service.isEnd(procInstId));
    }

    /**
     * <pre>
     * 流程测试2：开始==>申请==>受理==>定额==>结束
     * 主要涉及到的API：
     * 1、claimByTaskId(String taskId, String userId)：领取任务，相当于将任务指派给操作用户，但是流程还是停留在原节点，不会推进流程
     * 2、completeByTaskId(String taskId, String userId)：正常流程完成任务，按工作流引擎推进流程
     * 3、completeByTaskId(String taskId, int action, String userId)：正常流程完成任务，按工作流引擎推进流程（根据action走不同的分支）
     * </pre>
     */
    @Ignore
    @Test
    public void testProcess2() {
        Map<String, Object> variables = null;
        String businessKey = "workflow-demo-key-" + Utils.randomNumeric(9);

        // 启动流程实例，在调用此API之前，应该已经写入业务数据表了，根据流程图，启动一个流程实例，只会产生一个apply节点的任务
        ProcessInstance instance = service.startProcessInstanceByKey(PROC_DEF_KEY, variables, businessKey, USER_ID);
        String procInstId = instance.getProcessInstanceId();
        Task applyTask = this.assertAndReturnSingleTask(instance.getProcessInstanceId(), "apply");

        // 领取申请任务，不会推进流程
        this.service.claimByTaskId(applyTask.getId(), USER_ID);
        // 完成申请任务
        this.service.completeByTaskId(applyTask.getId(), USER_ID);
        // 根据流程图，完成apply之后，该流程实例将会产生一笔accept(受理)节点的任务
        Task acceptTask = this.assertAndReturnSingleTask(procInstId, "accept");

        // 直接完成accept任务（先不领取）
        this.service.completeByTaskId(acceptTask.getId(), USER_ID);
        // 根据流程图，完成accept之后，该流程实例将会产生一笔quota（定额）节点的任务
        Task quotaTask = this.assertAndReturnSingleTask(procInstId, "quota");

        // 直接完成quota任务、带参数action=0，根据流程图，流程结束
        int action = 0;
        this.service.completeByTaskId(quotaTask.getId(), action, USER_ID);
        assertProcessEnd(procInstId);
    }

    /**
     * <pre>
     * 流程测试3：开始==>申请==>受理==>定额==（跳跃至）==>制卡==(回退)==>定额==>审批==>制卡==>发卡==>结束
     * 主要涉及到的API：completeByTaskId(String taskId, String userId, int action, String destTaskKey)：调用者确定目标节点，可用于跳跃、回退等
     * </pre>
     */
    //@Ignore
    @Test
    public void testProcess3() {
        Map<String, Object> variables = null;
        String businessKey = "workflow-demo-key-" + Utils.randomNumeric(9);

        // 启动流程实例，在调用此API之前，应该已经写入业务数据表了，根据流程图，启动一个流程实例，只会产生一个apply节点的任务
        ProcessInstance instance = service.startProcessInstanceByKey(PROC_DEF_KEY, variables, businessKey, USER_ID);
        String procInstId = instance.getProcessInstanceId();
        Task applyTask = this.assertAndReturnSingleTask(instance.getProcessInstanceId(), "apply");

        // 完成申请任务，根据流程图，完成apply之后，该流程实例将会产生一笔accept(受理)节点的任务
        this.service.completeByTaskId(applyTask.getId(), USER_ID);
        Task acceptTask = this.assertAndReturnSingleTask(procInstId, "accept");

        // 直接完成accept任务（先不领取），根据流程图，完成accept之后，该流程实例将会产生一笔quota（定额）节点的任务
        this.service.completeByTaskId(acceptTask.getId(), USER_ID);
        Task quotaTask = this.assertAndReturnSingleTask(procInstId, "quota");

        // 直接完成quota任务、跳过审批，直接跳至制卡环节
        // 需要在ACT_TASK_FLOW中启用 quota-->make-card的流程
        int action = 1;//正向跳转
        this.service.completeFreedomByTaskId(quotaTask.getId(), action, USER_ID, "make-card");
        Task markCardTask = this.assertAndReturnSingleTask(procInstId, "make-card");

        // 从制卡回退至定额，产生新的定额任务
        // 需要在ACT_TASK_FLOW中启用 make-card-->quota的回退流程
        action = 0;//回退
        this.service.completeFreedomByTaskId(markCardTask.getId(), action, USER_ID, "quota");
        quotaTask = this.assertAndReturnSingleTask(procInstId, "quota");

        // 定额-审批，正常的流转，根据流程图，action==1时下一环节为审批
        action = 1;
        this.service.completeByTaskId(quotaTask.getId(), action, USER_ID);
        Task approveTask = this.assertAndReturnSingleTask(procInstId, "approve");

        // 审批-制卡，正常的流转，根据流程图，action==1时下一环节为制卡
        action = 1;
        this.service.completeByTaskId(approveTask.getId(), action, USER_ID);
        markCardTask = this.assertAndReturnSingleTask(procInstId, "make-card");

        //制卡-发卡，正常的流转，根据流程图，完成制卡后下一环节为发卡
        this.service.completeByTaskId(markCardTask.getId(), USER_ID);
        Task sendCardTask = this.assertAndReturnSingleTask(procInstId, "send-card");

        // 完成发卡任务，根据流程图，流程结束
        this.service.completeByTaskId(sendCardTask.getId(), USER_ID);
        assertProcessEnd(procInstId);

        this.printTrace(procInstId);
    }

    /**
     * 获取流程图，用于可视化的流程追踪
     * 
     * @throws IOException
     * @throws FileNotFoundException
     */
    @Ignore
    @Test
    public void testLoadResource() throws FileNotFoundException, IOException {
        // 使用流程定义Key获取流程图，是一个静态的流程图，是相同Key的最新流程图
        byte[] procDefKeyBytes = this.service.loadResourceByProcDefKey(PROC_DEF_KEY);
        StreamUtils.copy(procDefKeyBytes, new FileOutputStream(PROC_DEF_KEY + ".png"));

        // 使用流程实例ID获取流程图，不同的状态，流程图也不相同
        String procInstId = "4f4cf8b5-9604-11e8-a56f-646e69e8b476";
        byte[] procInstIdBytes = this.service.loadResourceByProcInstId(procInstId);
        StreamUtils.copy(procInstIdBytes, new FileOutputStream(procInstId + ".png"));
    }

    /**
     * <pre>
     * 获取流程跟踪信息
     * </pre>
     */
    @Ignore
    @Test
    public void testLoadProcessTrace() {
        String procInstId = "4f4cf8b5-9604-11e8-a56f-646e69e8b476";
        ProcessTrace trace = this.service.loadProcessTrace(procInstId);
        System.out.println(trace);
    }

    private void printTrace(String procInstId) {
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss:SSS");
        List<HistoricActivityInstance> list = this.engine.getHistoryService().createHistoricActivityInstanceQuery().processInstanceId(procInstId).orderByHistoricActivityInstanceStartTime().asc().list();
        StringBuilder sb = new StringBuilder();
        sb.append("\nprocInstId:").append(procInstId);
        for (HistoricActivityInstance act : list) {
            sb.append("\n\t").append(act.getActivityName())//
                    .append("[").append(act.getActivityId()).append("\t]{").append("actType: ").append(act.getActivityType())//
                    .append(",startTime: ").append(df.format(act.getStartTime()))//
                    .append(",endTime: ").append(df.format(act.getEndTime()))//
                    .append(",memo: ").append(act.getDeleteReason()).append("}");
        }
        System.out.println(sb.toString());
    }

    /**
     * 获取第一笔申请任务
     * 
     * @return
     */
    @SuppressWarnings("unused")
    private Task getFirstApplyTask() {
        // 根据流程定义KEY查询最新的流程定义
        ProcessDefinition processDefinition = this.service.queryProcessDefinition(PROC_DEF_KEY);

        String procDefId = processDefinition.getId();
        String taskDefKey = "apply";
        // 查询处于apply环节的任务，在实际开发中，往往不会直接调用工作流的API，而是关联工作流表，并添加业务条件（如机构、角色等）
        List<Task> applyTasks = engine.getTaskService().createTaskQuery().processDefinitionId(procDefId).taskDefinitionKey(taskDefKey).list();
        Assert.assertTrue(null != applyTasks && !applyTasks.isEmpty());
        return applyTasks.get(0);
    }

    /**
     * 断言流程实例只有一个taskDefinitionKey节点的任务，并返回这个任务
     * 
     * @param procInstId
     * @param taskDefinitionKey
     * @return
     */
    private Task assertAndReturnSingleTask(String procInstId, String taskDefinitionKey) {
        List<Task> tasks = engine.getTaskService().createTaskQuery().processInstanceId(procInstId).list();
        // 断言只有一个任务
        Assert.assertTrue(null != tasks && tasks.size() == 1);
        // 断言任务Key
        Assert.assertEquals(taskDefinitionKey, tasks.get(0).getTaskDefinitionKey());
        return tasks.get(0);
    }

    /**
     * 断言流程实例已经结束
     * 
     * @param procInstId
     */
    private void assertProcessEnd(String procInstId) {
        // 没有流程任务
        List<Task> tasks = engine.getTaskService().createTaskQuery().processInstanceId(procInstId).list();
        Assert.assertTrue(null == tasks || 0 == tasks.size());
        // 流程结束时间不为null
        HistoricProcessInstance instance = engine.getHistoryService().createHistoricProcessInstanceQuery().processInstanceId(procInstId).singleResult();
        Assert.assertNotNull(instance.getEndTime());
    }
}
